%% Author: Francis Stephens
%% Created: 28 Jan 2009
%% Description: TODO: Add description to compare_solvers
-module(compare_solvers).

%%
%% Include files
%%

-include("sat_macros.hrl").
-include("sat_records.hrl").
-include("worksharing_records.hrl").
-include("process_tree_records.hrl").
-include("process_list_records.hrl").
-include("dp_engine_concurrent_records.hrl").
-include("distributed_list_records.hrl").

%%
%% Exported Functions
%%
-export([
				run/0,
		 		run/1,
				run/2,
				run/3,
				run/5,
        compare_for_formulae/2,
        generate_formulae_file/1,
        profile_run_times/2
			]).

-compile(export_all).

%%
%%	Records
%%

%%
%%	Macros
%%
-define(TEST_FORMULAE_FILE_PATH,"test_formulae").
-define(TEST_FORMULAE_FILE_NAME,"formula_file.txt").

%%
%%	API Functions
%%
run() ->
	Formulae = formula_util:consult_formulae_file(?TEST_FORMULAE_FILE_NAME,?TEST_FORMULAE_FILE_PATH),
	{ok,Nodes} = file:script("nodes.txt"), %% This line is delicate and will cause you lots of problems :)
	Distributed_Config = organise_distributed_configs(Nodes,-1,2),
	set_up_and_compare(Formulae,-1,Distributed_Config,15).

run_local() ->
	run(1,4).

run(Var_Num_List,C_Num_List,Formula_Num) ->
	run(Var_Num_List,C_Num_List,Formula_Num,1,16).

run(Var_Num_List,C_Num_List,Formula_Num,Node_Num,Process_Num) ->
	%% Create all combinations of variable and clause counts
    Args_Comb = [{Var_Num,C_Num,3}||Var_Num<-Var_Num_List,C_Num<-C_Num_List],
	Solve_And_Measure = fun({Vnum,Cnum,Csize}) ->
                            Formulae = generate(Vnum,Cnum,Csize,Formula_Num),
							Node_Names = auto_generate_node_names(Node_Num),
							Distributed_Config = organise_distributed_configs(Node_Names,Process_Num,2),
							set_up_and_compare(Formulae,Process_Num,Distributed_Config,50)
						end,
	lists:foreach(Solve_And_Measure,Args_Comb).

%%
%%	Returns ok.
%%	Prints to the console a list of run times and a total time taken to solve a list of formulae.
%%	Automatically generates a list of node names
%%
run(Comm_Frequency) ->
	Formulae = formula_util:consult_formulae_file(?TEST_FORMULAE_FILE_NAME,?TEST_FORMULAE_FILE_PATH),
	{ok,Nodes} = file:script("nodes.txt"), %% This line is delicate and will cause you lots of problems :)
	Distributed_Config = organise_distributed_configs(Nodes,-1,2),
	set_up_and_compare(Formulae,-1,Distributed_Config,Comm_Frequency).

%% NB: Currently Process_Num is ignored
run(Node_Num,Process_Num) when is_integer(Node_Num) ->
    Formulae = formula_util:consult_formulae_file(?TEST_FORMULAE_FILE_NAME,?TEST_FORMULAE_FILE_PATH),
	Node_Names = auto_generate_node_names(Node_Num),
	Distributed_Config = organise_distributed_configs(Node_Names,Process_Num,2),
	set_up_and_compare(Formulae,Process_Num,Distributed_Config,15);

run(Node_Names,Process_Num) when is_list(Node_Names)->
	Formulae = formula_util:consult_formulae_file(?TEST_FORMULAE_FILE_NAME,?TEST_FORMULAE_FILE_PATH),
	Distributed_Config = organise_distributed_configs(Node_Names,Process_Num,2),
	set_up_and_compare(Formulae,Process_Num,Distributed_Config,15).

set_up_and_compare(Formulae,Process_Num,Distributed_Config,Comm_Frequency) ->
	%% Choose from our premuim range of Davis Putnam variations
  Most_Common_First_Dp = most_common_first_dp:get_functions(),
	No_Cp_Most_Common_First_Dp = no_cp_most_common_first_dp:get_functions(),
  Two_Variable_Branch_Dp = two_variable_branch_dp:get_functions(),
	No_Cp_Two_Variable_Branch_Dp = no_cp_two_variable_branch_dp:get_functions(),
	%% Organise the Config for the distributed case
	%% Organise the #engine_concurrent_args for each of our three different concurrency modes
	List_Network_Config = #list_network_config{network_size=Process_Num,
                    	work_sharing_threshold=2,
                    	work_splitting_fun=fun(List)->util:split_alternate(List)end},
	Process_List_Args = #engine_concurrent_args{network_config=List_Network_Config,comm_freq=Comm_Frequency,process_tree_mod=process_list},
	Distributed_List_Args = #engine_concurrent_args{network_config=Distributed_Config,comm_freq=Comm_Frequency,process_tree_mod=distributed_process_list},
  %% Finally set up a (nearly) complete array of all possible engine configurations
  Most_Common_First_Dp_List = erlang:setelement(#dp_funs.engine_args,Most_Common_First_Dp,Process_List_Args),
	Most_Common_First_Dp_Distributed = erlang:setelement(#dp_funs.engine_args,Most_Common_First_Dp,Distributed_List_Args),
  No_Cp_Most_Common_First_Dp_List = erlang:setelement(#dp_funs.engine_args,No_Cp_Most_Common_First_Dp,Process_List_Args),
	No_Cp_Most_Common_First_Dp_Distributed = erlang:setelement(#dp_funs.engine_args,No_Cp_Most_Common_First_Dp,Distributed_List_Args),
  Two_Variable_Branch_Dp_List = erlang:setelement(#dp_funs.engine_args,Two_Variable_Branch_Dp,Process_List_Args),
	Two_Variable_Branch_Dp_Distributed = erlang:setelement(#dp_funs.engine_args,Two_Variable_Branch_Dp,Distributed_List_Args),
	No_Cp_Two_Variable_Branch_Dp_List = erlang:setelement(#dp_funs.engine_args,No_Cp_Two_Variable_Branch_Dp,Process_List_Args),
	No_Cp_Two_Variable_Branch_Dp_Distributed = erlang:setelement(#dp_funs.engine_args,No_Cp_Two_Variable_Branch_Dp,Distributed_List_Args),
	Results = compare_for_formulae(Formulae,[{"Distributed",No_Cp_Two_Variable_Branch_Dp_Distributed,dp_engine_concurrent}]),
	analyse_and_print_results(Results).

auto_generate_node_names(Node_Num) ->
	Node_Numbers = lists:seq(1, Node_Num),
	{ok,Host_Name} = inet:gethostname(),
	lists:map(fun (Num) -> Pre = util:int_to_string(Num), Name = Pre ++ "@" ++ Host_Name, list_to_atom(Name) end, Node_Numbers).

organise_distributed_configs(Nodes,Process_Num,Work_Sharing_Threshold) ->
	List_Network_Config = #list_network_config{network_size=Process_Num,
                    		work_sharing_threshold=Work_Sharing_Threshold,
                    		work_splitting_fun=fun(List)->util:split_alternate(List)end},
	List_Network_Configs = util:generate_numbered_list(fun(Num) -> 
															   erlang:setelement(#list_network_config.log_name,List_Network_Config,util:format_int(Num)++"_list_log.LOG") 
													   end, length(Nodes)),
	#distributed_network_config{nodes=Nodes,list_network_configs=List_Network_Configs}.

analyse_and_print_results(Results) ->
	Extract_Time = fun({_Name,Time,_Solved}) ->
						   Time
				   end,
	Extract_Solved = fun({_Name,_Time,Solved}) ->
						   Solved
				   end,
	Extract_Name = fun({Name,_Time,_Solved}) ->
						   Name
				   end,
	Times = lists:map(fun(Run)-> lists:map(Extract_Time,Run) end,Results),
	Solveds = lists:map(fun(Run)-> lists:map(Extract_Solved,Run) end,Results),
	Names = lists:map(Extract_Name,hd(Results)),
	io:format("~nRaw Times! ~n~n~w~n",[lists:flatten(Times)]),
	io:format("~nRaw Times! ~n~n~p~n",[lists:map(fun(Time)-> util:format_int(Time) end,lists:flatten(Times))]),
	io:format("~n~n*************************************~n~n"),
	Means = stats_util:mean_vertical_2d(Times),
	Named_Means = lists:zip(Names, Means),
	Compare_Performance = fun(Run) ->
							  Run_Mean = stats_util:mean(Run),
							  lists:map(fun(Run_Time) -> Run_Time/Run_Mean end, Run)
					  end,
	Performance_Comparisons = lists:map(Compare_Performance, Times),
	io:format("~n~n~p~nResults = ~n~p~n~nMeans = ~n~p~n~nPerformance Comparisons = ~p~n~n",[Names,Results,Named_Means,Performance_Comparisons]),
	ok.

%%
%%	
%%
test(Formula) ->
    Generated_Formula = {generated_formula,Formula,undefined,undefined},
    Dp_Funs = most_common_first_dp:get_functions(),
    Default_Args = dp_engine_concurrent:get_default_args(),
    Process_Tree_Args = erlang:setelement(#engine_concurrent_args.process_tree_mod,Default_Args,process_tree),
    Dp_Funs_PT = erlang:setelement(#dp_funs.engine_args,Dp_Funs,Process_Tree_Args),
    Fun = fun() -> compare_for_formulae([Generated_Formula],[{"Con = ",Dp_Funs_PT,dp_engine_concurrent}]) end,
    util:do_n_times(Fun,1).

%%
%%	Returns a [{#generated_formula[{String(),Integer(),Bool()}]}]
%%
compare_for_formulae(Generated_Formulae,Names_And_Dp_Funs) ->
	io:format("~n~nRun Configuration = ~p~n~n",[Names_And_Dp_Funs]),
	Solve = 	fun(Generated_Formula) ->
                    Solve_And_Print = fun({Name,Dp_Funs,Engine_Mod}) ->
                                  			{Time,Sat_Result} = test_util:solve_and_time(Dp_Funs,Generated_Formula,Engine_Mod),
                                            %%io:format("~n~nSat_Result = ~p~n",[Sat_Result]),
                                            %%io:format("~n~n~p~p",[Name,util:format_int(Time)]),
                                  			{Name,Time,Sat_Result#sat_result.satisfiable}
                         			  end,
                    Formula_Run = lists:map(Solve_And_Print,Names_And_Dp_Funs),
                    sanity_check(Formula_Run,Generated_Formula),
                    Formula_Run
				end,
    lists:map(Solve,Generated_Formulae).

%%
%%	Creates a file "C:/Sat_Test/formulae/formula_file.txt" containing a list of formulae
%%	which will be used by the run/0 function.
%%	Standard number of clauses for a formula with 100 variables is 428,427
%%	
generate_formulae_file(Formula_Num) ->
    generate_formulae_file(Formula_Num,150,[642,640,641]).

generate_formulae_file(Formula_Num,Var_Num,Clause_Num) ->
	io:format("~n~nGenerating ~p formulae with ~p variables and ~p clauses~n~n",[Formula_Num,Var_Num,Clause_Num]),
    formula_util:make_formulae_file(Var_Num,Clause_Num,3,Formula_Num,?TEST_FORMULAE_FILE_NAME,?TEST_FORMULAE_FILE_PATH).
  
%%
%%	Calls get run times with profiling
%%
profile_run_times(Node_Num,Process_Num) ->
    eprof:profile([[Node_Num,Process_Num]],fun run/1),
    eprof:analyse().

%%
%%	Tests the speed of randomly assigning variables to a formula data-structure
%%
test_assignments() ->
    %% Use these variables to determine the formula datastructure
    Data_Structure = ?FORMULA_DATA_STRUCTURE,
    %% The actual test below
    Generated_Formulae = formula_util:consult_formulae_file(?TEST_FORMULAE_FILE_NAME,?TEST_FORMULAE_FILE_PATH),
    Generated_Formula = hd(Generated_Formulae),
    Formula = Data_Structure:def_2_formula(Generated_Formula#generated_formula.formula_def),
    Do_Assignment = fun() ->
							Variables = Data_Structure:get_variables(Formula),
                            [Variable] = util:random_sublist(1,Variables),
                            Boolean = util:int_to_bool(random:uniform(9)),
                            Variable_Assignment = #variable_assignment{l_name=Variable,value=Boolean},
                            ?FORMULA_DATA_STRUCTURE:assign_variable(Variable_Assignment,Formula)
                    end,
    {Time,_Return} = timer:tc(util,do_n_times,[Do_Assignment,1000]),
    util:format_int(Time).

%%
%%	Tests the speed of assigning each variable in a formula data-structure until all
%%	variables have been assigned.
%%
test_ass_all() ->
    %% Use these variables to determine the formula datastructure
    Data_Structure = cnf_graph_array,
    %% The actual test below
    Generated_Formulae = formula_util:consult_formulae_file(?TEST_FORMULAE_FILE_NAME,?TEST_FORMULAE_FILE_PATH),
    Generated_Formula = hd(Generated_Formulae),
    Formula = Data_Structure:def_2_formula(Generated_Formula#generated_formula.formula_def),
    Variable_Names = Data_Structure:get_variables(Formula),
    Assignment_Counts = lists:seq(1,length(Variable_Names)),
    Names_And_Counts = lists:zip(Variable_Names,Assignment_Counts),
    Assignment_Fun = fun({Variable_Name,Assignment_Count},Acc_Formula) ->
                            Do_Assignment = fun() ->
                                                    Boolean = util:int_to_bool(random:uniform(9)),
                            						Variable_Assignment = #variable_assignment{l_name=Variable_Name,value=Boolean},
                           		 					?FORMULA_DATA_STRUCTURE:assign_variable(Variable_Assignment,Acc_Formula)
                                            end,
                            util:do_n_times(Do_Assignment,math:pow(2,Assignment_Count)),
                      		Boolean = util:int_to_bool(random:uniform(9)),
                            Variable_Assignment = #variable_assignment{l_name=Variable_Name,value=Boolean},
                           	?FORMULA_DATA_STRUCTURE:assign_variable(Variable_Assignment,Acc_Formula)
                    end,
    Single_Run = fun() ->
                        lists:foldl(Assignment_Fun,Formula,Names_And_Counts)
                 end,
    {Time,_Return} = timer:tc(util,do_n_times,[Single_Run,1]),
    io:format("~n~n~p~n~n",[_Return]),
    util:format_int(Time).

%%
%%
%%
generate(Var_Num,C_Num,C_Size,Formula_Num) ->
    formula_generator:generate_formulae(Var_Num,C_Num,C_Size,Formula_Num).

%%
%%
%%
sanity_check(Formula_Run,Generated_Formula) ->
	All_Sats = [Sat||{_Name,_Time,Sat}<-Formula_Run],
	case util:homogenous_list(All_Sats) of
		false
	    	->	io:format("~n~nFuck!!!!!!!!!!!!!!~n~nTwo solvers have different opinions about a formula~n~nSolver results = ~p~n~n",[Formula_Run]),
	        	print_formula_to_file(Generated_Formula);
	    true
	    	->	ok
	end.

%%
%%	
%%
print_formula_to_file(Generated_Formula) ->
    {A,B,C} = now(),
    random:seed(A,B,C),
    Random_Number = util:format_int(random:uniform(1000)),
    {ok,File} = util:make_file(Random_Number ++ "Error_Formula.txt",test_util:get_file_path() ++ "/error"),
    {Module,F_Name} = (Generated_Formula#generated_formula.print_generated_formula),
	Module:F_Name(File,Generated_Formula),
	file:close(File).

%%
%%
%%
sum_file(File_Name) ->
		{ok, List} = file:script(File_Name),
		lists:sum(List).